package com.rgbgame.physics;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * <p>
 * A dispatcher for pairwise listening of collision events.
 * </p>
 * <p>
 * Types of filters you might want to use for listening include:
 * <ul>
 * <li>By unique ID (m_id):
 * <ul>
 * <li>Specific actor vs. specific actor (player hits a switch, for instance)
 * <li>Specific shape vs. specific actor (player's sword hits a switch, but hand doesn't trigger it)
 * <li>Specific shape vs. specific shape (player's head hits another player's head)
 * </ul>
 * <li>By group (m_groupID)
 * </ul>
 *
 */
public class ContactEventDispatcher {
	
	HashMap<Integer,ArrayList<ContactEventListener>> specificActorActorListeners;
	HashMap<Integer,ArrayList<ContactEventListener>> specificShapeShapeListeners;
	HashMap<Integer,ArrayList<ContactEventListener>> specificActorShapeListeners;
	ArrayList<ArrayList<ContactEventListener>> groupListeners;
	
	public ContactEventDispatcher() {
		specificActorActorListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		specificShapeShapeListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		specificActorShapeListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		groupListeners = new ArrayList<ArrayList<ContactEventListener>>();
		for (int i=0; i<Actor.MAX_GROUPS; ++i) {
			groupListeners.add(new ArrayList<ContactEventListener>());
		}
	}
	
	/**
	 * Send this contact event out to all interested listeners.
	 * @param c The RGBContactPoint to send
	 */
	public void dispatchEvent(RGBContactPoint c) {
		ShapeData s1 = (ShapeData)(c.shape1.getUserData());
		ShapeData s2 = (ShapeData)(c.shape2.getUserData());
		Actor a1 = (Actor)(c.shape1.getBody().getUserData());
		Actor a2 = (Actor)(c.shape2.getBody().getUserData());
		int s1id = s1.getID();
		int s2id = s2.getID();
		int a1id = a1.getID();
		int a2id = a2.getID();
		sendEventMessages(specificActorActorListeners,a1id,a2id,c);
		sendEventMessages(specificShapeShapeListeners,s1id,s2id,c);
		sendEventMessages(specificActorShapeListeners,a1id,s2id,c);
		sendEventMessages(specificActorShapeListeners,a2id,s1id,c);
		int flags1 = s1.getGroupFlags();
		int flags2 = s2.getGroupFlags();
		int flags3 = a1.getGroupFlags();
		int flags4 = a2.getGroupFlags();
		int groupsToSendTo = flags1 | flags2 | flags3 | flags4;
		sendGroupEventMessages(groupsToSendTo,c);
	}
	
	/**
	 * Add a listener that will report all contact events involving a certain
	 * group of shapes or actors.
	 * @param group The group number to listen for (NOT the group flags)
	 * @param listener
	 */
	public void addGroupListener(int group, ContactEventListener listener) {
		assert(group < Actor.MAX_GROUPS);
		groupListeners.get(group).add(listener);
	}
	
	public void removeGroupListener(int group, ContactEventListener listener) {
		assert(group < Actor.MAX_GROUPS);
		groupListeners.get(group).remove(listener);
	}
	
	private void sendGroupEventMessages(int groupFlags,RGBContactPoint c) {
		if (groupFlags == 0) return;
		int mask = 1 << 0;
		for (int i=0; i<Actor.MAX_GROUPS; ++i) {
			if ( (mask & groupFlags) == mask ) {
				// Fire event
				ArrayList<ContactEventListener> toTrigger = groupListeners.get(i);
				int toTriggerLength = toTrigger.size();
				for (int j=0; j<toTriggerLength; ++j) {
					toTrigger.get(j).trigger(c);
				}
			}
			mask = mask << 1;
		}
	}
	
	private void sendEventMessages(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id1, int id2, RGBContactPoint c) {
		int hashed = hash(id1,id2);
		ArrayList<ContactEventListener> list = hashMap.get(hashed);
		if (list == null) return;
		for (int i=0; i<list.size(); ++i) {
			list.get(i).trigger(c);
		}
	}
	
	private void registerListener(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id1, int id2, ContactEventListener listener) {
		int hashed = hash(id1,id2);
		if (hashMap.containsKey(hashed)) {
			hashMap.get(hashed).add(listener);
		} else {
			ArrayList<ContactEventListener> list = new ArrayList<ContactEventListener>();
			list.add(listener);
			hashMap.put(hashed, list);
		}
	}
	
	/**
	 * Registers a contact event listener to respond to events between
	 * actors with specified id's (use Actor::getID() to find these).
	 * Do not register the same listener more than once for the same pair!
	 * Order does not matter - the listener is registered for both orderings.
	 * @param id1
	 * @param id2
	 * @param listener
	 */
	public void registerSpecificActorActorListener(int id1, int id2, ContactEventListener listener) {
		registerListener(specificActorActorListeners,id1,id2,listener);
		if (id2 != id1) registerListener(specificActorActorListeners,id2,id1,listener);
	}
	
	/**
	 * Registers a contact event listener to respond to events between
	 * shapes with specified id's (use Shape::getID() to find these).
	 * Do not register the same listener more than once for the same pair!
	 * Order does not matter - the listener is registered for both orderings.
	 * @param id1
	 * @param id2
	 * @param listener
	 */
	public void registerSpecificShapeShapeListener(int id1, int id2, ContactEventListener listener) {
		registerListener(specificShapeShapeListeners,id1,id2,listener);
		if (id2 != id1) registerListener(specificShapeShapeListeners,id2,id1,listener);
	}
	
	/**
	 * Registers a contact event listener to respond to events between
	 * actors and shapes with specified id's (use Actor/Shape::getID() to find these).
	 * Do not register the same listener more than once for the same pair!
	 * @param id1 The actor's id
	 * @param id2 The shape's id
	 * @param listener
	 */
	public void registerSpecificActorShapeListeners(int id1, int id2, ContactEventListener listener) {
		registerListener(specificActorShapeListeners,id1,id2,listener);
	}
	
	private void unregisterListener(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id1, int id2, ContactEventListener listener) {
		int hashed = hash(id1,id2);
		if (hashMap.containsKey(hashed)) {
			ArrayList<ContactEventListener> list = hashMap.get(hashed);
			if (list.remove(listener)) {
				// Success - check if the arraylist is now empty, in which case
				// we can remove the arraylist from the map.
				if (list.size() == 0) hashMap.remove(hashed);
			} else {
				throw new RuntimeException("Listener was not found in array list.");
			}
		} else {
			throw new RuntimeException("Key was not found in hash map.");
		}
	}
	
	public void unregisterSpecificActorActorListener(int id1, int id2, ContactEventListener listener) {
		unregisterListener(specificActorActorListeners,id1,id2,listener);
	}
	
	public void unregisterSpecificShapeShapeListener(int id1, int id2, ContactEventListener listener) {
		unregisterListener(specificShapeShapeListeners,id1,id2,listener);
	}
	
	public void unregisterSpecificActorShapeListeners(int id1, int id2, ContactEventListener listener) {
		unregisterListener(specificActorShapeListeners,id1,id2,listener);
	}
	
	/**
	 * Two element (int,int)->int hash.
	 * 
	 * @param id1 First value in pair to hash
	 * @param id2 Second value in pair to hash
	 * @return Hash value
	 */
	static public final int hash(int id1, int id2) {
        int key = (id2 << 16) | id1;
        key = ~key + (key << 15);
        key = key ^ (key >>> 12);
        key = key + (key << 2);
        key = key ^ (key >>> 4);
        key = key * 2057;
        key = key ^ (key >>> 16);
        return key;
    }
}
